<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>Document</title>
        <link rel="stylesheet" href="./banner.css" />
    </head>

    <body>

        <div class="banner">
            <ul class="imgBox clearFix">
                <li style="background-color: red">1</li>
                <li style="background-color: orange">2</li>
                <li style="background-color: green">3</li>
                <li style="background-color: pink">4</li>
                <li style="background-color: yellow">5</li>
            </ul>

            <!-- 指示器存放处 -->
            <ol class="pointBox"></ol>

            <div class="leftRightTabs">
                <a href="javascritp:;" class="left"><</a>
                <a href="javascritp:;" class="right">></a>
            </div>
        </div>

        <script>
            // 最外层的盒子
            const bannerBox = document.querySelector(".banner");

            // 幻灯片slider所在的盒子 ul
            const sliderBox = bannerBox.querySelector("ul");

            // 指示器容器
            const pointerBox = bannerBox.querySelector("ol");

            // 左右按钮
            const leftRightTabs = bannerBox.querySelector(".leftRightTabs");

            // 全部滑动页
            const sliders = document.querySelectorAll("ul>li");
            const sliderWidth = parseInt(
                window.getComputedStyle(sliders[0]).width
            );

            // 每2000毫秒轮播一次
            let interval = 2000;

            // 动画定时器
            let loopTimerId = 0;

            // 轮播的起始序号
            let index = 1;

            // 指示器的序号数组??
            const pointerIndexArr = [];

            // 标记是否有动画在跑（动画进行到一半的时候 疯狂点击左右键不予理会）
            let animIsRunning = false;

            // 在程序开始执行之前做好一切初始化工作
            main();

            /* 整个程序的入口 */
            function main() {
                initSliders();
                initListeners();
                initDragListener();
                initPointers();
                startAutoPlay();
            }

            /* 
           初始化 
           拷贝元素 为轮播做准备
           */
            function initSliders() {
                /* 拷贝sliders的头尾两个元素 1 5 */
                // 拷贝一个1
                let rightInsertNode = sliders[0].cloneNode(true);
                // 拷贝一个5
                let leftInsertNode = sliders[sliders.length - 1].cloneNode(true);

                // 把新拷贝出来的1插到原来5的右边
                sliderBox.appendChild(rightInsertNode);

                // 把拷贝的5插到原来的1的左边
                sliderBox.insertBefore(leftInsertNode, sliderBox.children[0]);

                // 重新设置一下UL的宽度 令其能包裹所有slider
                sliderBox.style.width = sliderBox.children.length * sliderWidth + "px";

                // slider初始化完成后 将默认页面摆在1上 index的默认值为1
                sliderBox.style.left = -sliderWidth * index + "px";
            }

            /* 开始自动播放 */
            function startAutoPlay() {
                /* 每2000毫秒向右滚动一下 相当于点击了向右按钮*/
                // 如果没有轮播在跑 才开始轮播
                if (loopTimerId === 0) {
                    loopTimerId = setInterval(() => {
                        moveRight();
                    }, interval);
                }
            }

            /* 停止轮播 */
            function stopAutoPlay() {
                // 有轮播在跑
                if (loopTimerId !== 0) {
                    // 干掉这个轮播
                    clearInterval(loopTimerId);

                    // 设置当前的轮播定时器ID为0
                    loopTimerId = 0;
                }
            }

            /* 动画结束回调函数 */
            function onMoveEnd() {
                // 动画停止时 最后一帧的状态距离目标值可能存在微小偏差（拖拽的时候的事件位置和speed都有小数）
                // 动画停止时 手动校正位置
                // 纯属轮播图自己的业务逻辑 不属于公共方法 move 的管辖范畴
                sliderBox.style.left = -sliderWidth * index + "px";

                // 更新指示器的高亮位置
                updatePointer();

                /* 
                当index来到6的时候 显示的是队尾的1 静默地将其置换为1号位置的1
                5 【1】 2 3 4 5 【1】
                   ↑-------------↑
                */
                if (index === sliderBox.children.length - 1) {
                    // 序号干回1（又可以接着往大浪了）
                    index = 1;

                    // 静默地 瞬间完成切换
                    sliderBox.style.left = -sliderWidth * index + "px";
                }

                if (index === 0) {
                    // 序号干回1（又可以接着往大浪了）
                    index = sliderBox.children.length - 2;

                    // 静默地 瞬间完成切换
                    sliderBox.style.left = -sliderWidth * index + "px";
                }
            }

            /* 初始化事件监听器 */
            function initListeners() {
                /* 禁止页面文本被选中的默认行为 */
                // 无关this 大胆用箭头函数
                document.onselectstart = (e) => {
                    e.preventDefault();
                };

                /* 监听页面的显示与否 如果显示则自动轮播 否则停止自动轮播（攒一大堆待渲染的效果 回到页面时会同时执行 导致页面乱跳） */
                // visible可见 invisible不可见 visibility可见性 state状态 hide隐藏 hidden被隐藏的

                // 监听文档的显隐事件
                document.onvisibilitychange = function (e) {
                    // 状态切换为可见时 开始自动轮播
                    if (document.visibilityState === "visible") {
                        // console.log("visible");
                        startAutoPlay();
                    }
                    // 状态切换为隐藏时 停止自动轮播
                    if (document.visibilityState === "hidden") {
                        // console.log("hidden");
                        stopAutoPlay();
                    }
                };
                
                /* 鼠标一旦移入imgBox 停止轮播 等待用户点击 */
                sliderBox.onmouseover = function (e) {
                    stopAutoPlay();
                };
                sliderBox.onmouseout = function (e) {
                    startAutoPlay();
                };
                
                leftRightTabs.onmouseover = function (e) {
                    stopAutoPlay();
                };

                /* 悬停在指示器上 停止自动轮播 阻止控制权的争夺 */
                pointerBox.onmouseover = function (e) {
                    stopAutoPlay();
                };

                /* 把左右两个按钮的点击事件委托给父元素 */
                leftRightTabs.onclick = function (e) {
                    // 只要有动画在跑 就啥也不干 直接结束
                    if (animIsRunning) {
                        // 直接结束
                        return;
                    }

                    // 根据类名拿到点击的a
                    if (e.target.className === "left") {
                        // 向左滑
                        moveLeft();
                    }

                    if (e.target.className === "right") {
                        // 向右滑
                        moveRight();
                    }
                };
            }

            /* 初始化拖拽监听 */
            function initDragListener() {
                /* 鼠标按下时 开始拖拽逻辑 */
                sliderBox.onmousedown = function (de) {
                    // 只要有动画在跑 就啥也不干 直接结束
                    if (animIsRunning) {
                        // 直接结束
                        return;
                    }
                    console.log("onmousedown");

                    // 记录鼠标按下时的事件位置
                    const startX = de.pageX;
                    // const startY = de.pageY
                    // console.log("鼠标按下时imgBox的left", imgBox.style.left);

                    let lastX = startX;

                    // 开始拖动 imgBox跟着鼠标走
                    sliderBox.onmousemove = function (me) {
                        // 调出上一次的事件位置 求得本次事件与上一次的偏移量
                        const thisOffset = me.pageX - lastX;

                        // 存下本次的事件位置 供下一次用计算偏移量
                        lastX = me.pageX;

                        // 计算imgBox应该去到的left
                        // console.log(
                        //     "鼠标移动时imgBox的left",
                        //     imgBox.style.left
                        // );
                        let targetLeft = parseFloat(sliderBox.style.left) + thisOffset;
                        // console.log(thisOffset, targetLeft);

                        // imgBox的偏移量设为鼠标左右滑动的偏移量
                        sliderBox.style.left = targetLeft + "px";
                    };

                    sliderBox.onmouseup = function (ue) {
                        // 只要有动画在跑 就啥也不干 直接结束
                        if (animIsRunning) {
                            // 直接结束
                            return;
                        }

                        // 鼠标已经抬起 盒子不用跟着我跑了
                        sliderBox.onmousemove = null;

                        // 计算出本次拖拽（从按下到抬起）的总偏移量
                        let dragOffset = ue.pageX - startX;

                        // 如果大幅向右 moveRight
                        if (dragOffset > sliderWidth / 3) {
                            moveLeft();
                        }

                        // 大幅向左 moveLeft
                        else if (dragOffset < -sliderWidth / 3) {
                            moveRight();
                        }

                        // 幅度太小 回到拖拽前的原位
                        else {
                            moveTo(index);
                        }
                    };
                };
            }
            
            /* 左滑 */
            function moveLeft() {
                // 已经在最左边缘了 不允许moveLeft
                if (index === 0 || animIsRunning) {
                    return;
                }

                console.log("moveLeft");
                index--;
                animIsRunning = true;

                // 调用动画 动画结束时回调updatePointer方法 刷新指示器高亮位置
                // imgBox.style.left = -boxWidth * index + "px";
                move(sliderBox, "left", -sliderWidth * index, "left", onMoveEnd);
            }

            /* 右滑 */
            function moveRight() {
                // 已经在最右边缘了 不允许moveRight
                if (index === sliderBox.children.length - 1 || animIsRunning) {
                    return;
                }

                console.log("moveRight");
                index++;
                animIsRunning = true;

                // 调用动画 动画结束时回调updatePointer方法 刷新指示器高亮位置
                // imgBox.style.left = -boxWidth * index + "px";
                move(sliderBox, "left", -sliderWidth * index, "right", onMoveEnd);
            }

            /* 
            element     要做动画的 元素
            attr        要做动画的属性
            target      目标属性值
            callback    当动画完成时回调的函数
            */
            function move(element, attr, target, direction, callback) {
                let currentValue = parseInt(
                    window.getComputedStyle(element)[attr]
                );
                let speed = (target - currentValue) / 50;

                let count = 0
                let animTimer = setInterval(() => {
                    currentValue += speed;
                    element.style[attr] = currentValue + "px";
                    count ++

                    // console.log("left,target", element.style[attr], target);
                    if (count >= 50) {
                        clearInterval(animTimer);

                        // 定时器已被清除 动画已结束
                        animIsRunning = false;
                        console.log("animIsRunning = false;");

                        // callback && callback()
                        if (callback) {
                            callback();
                        }
                    }
                }, 20);

                // 定时器开始跑 标记动画开始
                animIsRunning = true;
            }

            /* 初始化指示器 一排可以点击的小圆点 */
            function initPointers() {
                // 为每个滑动页slider配置一个指示器pointer
                sliders.forEach((silider, index) => {
                    // 创建一个LI元素并丢入pointerBox中（CSS样式已经提前预设好了）
                    var pointer = document.createElement("li");

                    // 每个pointer与滑动页的index一一对应
                    // 考虑到轮播页的加头尾操作 手动校正一下
                    // 0 1 2 3 4 5 6 index
                    // 5 1 2 3 4 5 1 字面
                    // x 1 2 3 4 5 x pointer中藏的“index”应该与滑动页的index完全对应 才能实现点哪个指示器 就跳转哪个滑动页
                    pointer.setAttribute("index", index + 1);

                    // 将当前的指示器LI添加到pointerBox这个OL中
                    pointerBox.appendChild(pointer);

                    // 将第一个小圆点高亮（因为默认选中第一张幻灯片）
                    if (index === 0) {
                        pointer.classList.add("active");
                    }
                });

                // 给OL设置宽度，让pointer之间有一定空隙（CSS样式已经提前预设好了）
                pointerBox.style.width = sliders.length * 20 + "px";

                // 准备速查表 index -> pointer index
                // 0 1 2 3 4 5 6 index
                // 5 1 2 3 4 5 1 字面
                // 4 0 1 2 3 4 0 pointerIndex
                // 制作一个由index检索pointerIndex的速查表
                // [4, 0, 1, 2, 3, 4, 0]
                for (let i = 0; i < sliderBox.children.length; i++) {
                    switch (i) {
                        case 0:
                            pointerIndexArr[i] = sliders.length - 1;
                            break;
                        case sliderBox.children.length - 1:
                            pointerIndexArr[i] = 0;
                            break;
                        default:
                            pointerIndexArr[i] = i - 1;
                            break;
                    }
                }
                // console.log(pointerIndexArr);

                // 将每个pointer的点击事件交给父元素pointerBox代理
                pointerBox.onclick = function (e) {
                    // 如果点击的末梢是一个LI（点击了指示器）
                    if (e.target.nodeName === "LI") {
                        // 取出该指示器的index
                        let targetIndex = e.target.getAttribute("index");
                        // 并滑动到对应的滑动页
                        moveTo(targetIndex);
                    }
                };
            }

            /* 一步到位地跳到指定页面 */
            function moveTo(n) {
                direction = index > n ? "left" : "right";
                // 设好index
                index = n;

                // 调用动画设置imgBox的正确位置
                // 动画结束之后 回调updatePointer()刷新指示器的高亮位置
                move(sliderBox, "left", -sliderWidth * index, direction, onMoveEnd);
            }

            /* 刷新指示器的高亮位置 */
            // 制作一个由index检索pointerIndex的速查表
            // 0 1 2 3 4 5 6 index
            // 5 1 2 3 4 5 1 字面
            // 4 0 1 2 3 4 0 pointerIndex
            // [4, 0, 1, 2, 3, 4, 0]
            function updatePointer() {
                // 遍历pointerBox中的全部pointer 全部取消active
                for (let i = 0; i < pointerBox.children.length; i++) {
                    // 拿到第I个LI/pointer
                    let pointer = pointerBox.children[i];
                    // 取消高亮
                    pointer.classList.remove("active");
                }

                // 根据当前index找到正确的pointer 并激活
                let pointerIndex = pointerIndexArr[index];
                pointerBox.children[pointerIndex].classList.add("active");
            }
        </script>
    </body>

</html>
